<?php
/**
* The Page model.
*
* Copyright (c) 2009 James Gauld <james@jamesgauld.net>
* This file is part of Scribble.
* @license http://www.scribblecms.co.uk/license.txt
*
* @package Scribble
*/
use Buan\Config;
use Buan\ExtensionManager;
use Buan\ModelCriteria;
use Buan\Model;
use Buan\ModelCollection;
use Buan\ModelException;
use Buan\ModelManager;
use Buan\ModelRelation;
use Buan\UrlCommand;
class PageModel extends ScribbleMasterModel {

	/**
	* Return a list of visible (moderated) comments, order by date added.
	* This is a convenient shortcut for getting moderated comments only, so if you
	* want to have more control over which comments are returned you'll need to
	* use Buan's core Model::findRelatives() and associated methods.
	*
	* @return Buan\ModelCollection.
	*/
	public function findModeratedComments() {
		$c = new ModelCriteria();
		$c->addClause(ModelCriteria::EQUALS, 'isvisible', 1);
		$c->addOrder('datecreated');
		return $this->findRelatives('PageComment', $c);
	}

	/**
	* A convenience method for finding all children of $this Page that are
	* published and also match the optional $criteria.
	*
	* @param Buan\ModelCriteria Criteria to apply to the search
	* @return Buan\ModelCriteria
	*/
	public function findPublishedChildren($criteria=NULL) {

		// Build criteria that will restrict results to only published Pages
		if($criteria===NULL) {
			$criteria = new ModelCriteria();
		}
		$criteria->addClause(ModelCriteria::GREATER_THAN, 'datepublish', 0);
		$criteria->addClause(ModelCriteria::LESS_THAN, 'datepublish', time());
		$g = $criteria->addGroup(ModelCriteria::LOGIC_OR);
		$g->addClause(ModelCriteria::EQUALS, 'dateexpire', 0);
		$g->addClause(ModelCriteria::GREATER_THAN, 'dateexpire', time());

		// Execute
		return $this->findRelatives('Page', $criteria, ModelRelation::REF_PARENT);
	}

	/**
	* Returns the absolute URL path to this page, with or without the URL suffix
	* defined when configuring Scribble
	*
	* @param bool If TRUE then the URL suffix will be omitted
	* @return string
	*/
	public function getAbsoluteUrl($omitSuffix=FALSE) {

		// Auto or custom-relative URL
		$url = Config::get('app.urlRoot');
		$suffix = $omitSuffix ? '' : Config::get('ext.Scribble.urlSuffix');
		if(!$this->hasCustomAbsoluteUrl()) {
			if($this->page_id!='') {
				$parent = $this->findRelatives('Page', ModelRelation::REF_CHILD);
				if(!$parent->isEmpty() && $parent->get(0)->isindex!=1) {
					$url .= $parent->get(0)->getAbsoluteUrl(TRUE);
				}
			}
			return $this->customurl=='' ? "{$url}/{$this->url}{$suffix}" : "{$url}/{$this->customurl}{$suffix}";
		}

		// Custom absolute URL
		else {
			return $this->customurl.$suffix;
		}
	}

	/**
	* Returns an array of Page models, from $this Page to it's root parent.
	* This is useful for passing as the resource argument to ScribbleAuth::authorize().
	*
	* @return array
	*/
	public function getPathToRoot() {
		$path = array();
		$p = $this;
		do {
			$path[] = $p;
			$p = $p->page_id!='' ? $p->findRelatives('Page', ModelRelation::REF_CHILD)->get(0) : NULL;
		} while($p!==NULL);
		return $path;
	}

	/**
	* Returns same result as PageModel::getPathToRoot(), but includes the Site
	* model to which the Pages belong at the end of the array.
	*
	* @return array
	*/
	public function getPathToSite() {
		$path = $this->getPathToRoot();
		$site = $this->findRelatives('Site');
		if(!$site->isEmpty()) {
			$path[] = $site->get(0);
		}
		return $path;
	}

	/**
	* Get the number of comments made on this Page.
	*
	* @param bool If TRUE then only comments with a "isvisible=1" flag are counted
	* @return int
	*/
	public function getCommentCount($moderatedOnly=FALSE) {
		$tp = Config::get('ext.Scribble.dbTablePrefix');
		$sql = "SELECT COUNT(*) AS c FROM {$tp}page_comment WHERE page_id=?";
		if($moderatedOnly) {
			$sql .= ' AND isvisible=1';
		}
		$stmt = ModelManager::sqlQuery($sql, array($this->id));
		return (int)$stmt->fetch(PDO::FETCH_OBJ)->c;
	}

	/**
	* Return an instance of PageContent model associated with $this Page.
	*
	* If no content is available, a new PageContent instance is created, attached
	* to $this page and returned. Note this instance will NOT have a template
	* specified so you must define this before saving.
	*
	* @param string Revision ID
	* @return Buan\Model
	*/
	/*public function getInnerContent($revision=NULL) {

		// Find the latest content model, or the revision specified
		$c = new ModelCriteria();
		$c->addClause(ModelCriteria::EQUALS, 'type', ScribbleTemplate::TYPE_INNER);
		if($revision!==NULL) {
			$c->addClause(ModelCriteria::EQUALS, 'id', $revision);
		}
		$c->addOrder('datecreated', 'desc');
		$c->setRange(0, 1);
		$cm = $this->findRelatives('PageContent', $c)->asArray();

		// No content found; attache and return new instance
		if(empty($cm)) {
			$cm = Model::create('PageContent');
			$cm->addRelatives($this);
			$cm->datecreated = time();
			$cm->type = ScribbleTemplate::TYPE_INNER;
			return $cm;
		}

		// Content found; return the first instance
		else {
			// This commented line MAY be required if not creating a brand new
			// revision because if !$page->hasChanged() then the save routine for
			// $content won't be triggered when $page itself is saved.
			//$page->addRelatives($content);

			return array_shift($cm);
		}
	}*/

	/**
	* Calling this method is the recommended way of getting hold of the front-end
	* View of this Page's inner template.
	*
	* @return ScribblePublicView
	* @throws Buan\ModelException if there's anything wrong with the template
	*/
	/*public function getInnerView() {
		try {
			$cm = $this->getInnerContent();
			$tpl = ScribbleTemplateManager::getTemplateByName($cm->template);
			return $tpl->getFrontEndView($cm);
		}
		catch(ScribbleTemplateException $e) {
			throw new ModelException($e->getMessage());
		}
	}*/

	/**
	* Return an instance of PageContent model associated with $this Page.
	*
	* If no content is available, a new PageContent instance is created, attached
	* to $this page and returned. Note this instance will NOT have a template
	* specified so you must define this before saving.
	*
	* @param string Revision ID
	* @return Buan\Model
	*/
	/*public function getOuterContent($revision=NULL) {

		// Find the latest content model, or the revision specified
		$c = new ModelCriteria();
		$c->addClause(ModelCriteria::EQUALS, 'type', ScribbleTemplate::TYPE_OUTER);
		if($revision!==NULL) {
			$c->addClause(ModelCriteria::EQUALS, 'id', $revision);
		}
		$c->addOrder('datecreated', 'desc');
		$c->setRange(0, 1);
		$cm = $this->findRelatives('PageContent', $c)->asArray();

		// No content found; attach and return new instance
		if(empty($cm)) {
			$cm = Model::create('PageContent');
			$cm->addRelatives($this);
			$cm->datecreated = time();
			$cm->type = ScribbleTemplate::TYPE_OUTER;
			return $cm;
		}

		// Content found; return the first instance
		else {
			// This commented line MAY be required if not creating a brand new
			// revision because if !$page->hasChanged() then the save routine for
			// $content won't be triggered when $page itself is saved.
			//$page->addRelatives($content);

			return array_shift($cm);
		}
	}*/

	/**
	* Calling this method is the recommended way of getting hold of the front-end
	* View of this Page's outer template.
	*
	* As well as generating the View, it will also do the following:
	* - Add an "X-Pingback" HTTP header, unless $this Page explicitly forbids it
	*
	* @return ScribblePublicView
	* @throws Buan\ModelException if there's anything wrong with the template
	*/
	/*public function getOuterView() {
		try {
			$cm = $this->getOuterContent();
			$tpl = ScribbleTemplateManager::getTemplateByName($cm->template);
			$view = $tpl->getFrontEndView($cm);
			if(($site = $this->findRelatives('Site')->get(0))!==NULL) {
				$view->setHeader('X-Pingback', "http://{$site->prefix}".UrlCommand::createUrl('pingback'));
			}
			return $view;
		}
		catch(ScribbleTemplateException $e) {
			throw new ModelException($e->getMessage());
		}
	}*/

	/**
	* Returns the permalink that can be used to access this Page. If $this Page
	* has not yet been saved to the database (and thus doesn't yet have a PK) then
	* the absolute URL will be returned instead.
	*
	* @return string
	*/
	public function getPermalinkUrl() {
		if(!$this->isInDatabase()) {
			return $this->getAbsoluteUrl();
		}
		else {
			return UrlCommand::createAbsoluteUrl('permalink', NULL, array('p'=>$this->permalink));
		}
	}

	/**
	* Build and return a history of this Page's content revisions. This returns an
	* array ordered from earliest to most recent revisions.
	*
	* Each element in the array is indexed sequentially and contains a StdClass
	* containing information about the content models at that revision in the
	* format:
	*		StdClass {
	*			string innerId: 'ID of the inner content model',
	*			string outerId: 'ID of the outer content model',
	*			int timestamp: Time at which this revision first appears (unix timestamp),
	*			string authorId: 'ID of the User who made this revision',
	*			string authorName: 'Name of the author'
	*		}
	*
	* @return array
	*/
	/*public function getRevisionHistory() {
		$history = array();
		$c = new ModelCriteria();
		$c->addOrder('datecreated');
		$revisions = $this->findRelatives('PageContent', $c);
		$latest = array(
			ScribbleTemplate::TYPE_OUTER=>NULL,
			ScribbleTemplate::TYPE_INNER=>NULL
		);
		foreach($revisions as $rev) {
			$latest[$rev->type] = $rev;
			if($latest[ScribbleTemplate::TYPE_OUTER]!==NULL && $latest[ScribbleTemplate::TYPE_INNER]!==NULL) {
				$history[] = (object)array(
					'outerId'=>$latest[ScribbleTemplate::TYPE_OUTER]->id,
					'innerId'=>$latest[ScribbleTemplate::TYPE_INNER]->id,
					'timestamp'=>$rev->datecreated,
					'authorId'=>$rev->author_id,
					'authorName'=>$rev->authorname
				);
			}
		}
		return $history;
	}*/

	/**
	* Return this Page's tags in a simple array.
	*
	* @return array
	*/
	/*public function getTagsArray() {
		$tagModels = $this->findRelatives('TagPage');
		$tags = array();
		foreach($tagModels as $tm) {
			$tags[] = $tm->tag;
		}
		return $tags;
	}*/

	/**
	* Intercept calls to "... = $this->targetmenus' to JSON-decode this Page's
	* list of target menus into an array.
	*
	* @return array
	*/
	/*public function getTargetmenus() {
		return !empty($this->dbData['targetmenus']) ? json_decode($this->dbData['targetmenus']) : array();
	}*/

	/**
	* Returns this Page's "handlesubpaths" settings, which defines whether or not
	* it can handle missing sub paths.
	*
	* For example, if this Page resides on the URL:
	*		/my/page.html
	* And a request is made for a non-existent resource:
	* 	/my/page/and/something.html
	* Then the "/and/something.html" part will be passed through as extra action
	* parameters when rendering $this Page.
	*
	* @return bool
	*/
	/*public function handleSubPaths() {
		return $this->handlesubpaths==1;
	}*/

	/**
	* Determine if this Page is using a custom absolute URL (ie. beginning with
	* a leading forward slash, "/")
	*
	* @return bool
	*/
	public function hasCustomAbsoluteUrl() {
		return preg_match("/^\/.*$/", $this->customurl);
	}

	/**
	* Determine if this Page is using a custom relative URL.
	*
	* @return bool
	*/
	public function hasCustomRelativeUrl() {
		return $this->customurl!='' && $this->customurl[0]!='/';
	}

	/**
	* Determine if this Page has expired.
	*
	* @return bool
	*/
	/*public function hasExpired($time=NULL) {
		$time = $time===NULL ? time() : $time;
		return $this->dateexpire>0 && $this->dateexpire<=$time;
	}*/

	/**
	* Return page id meta.
	*
	* @return StdClass
	*/
	public function idMeta() {
		$meta = parent::idMeta();
		$meta->name = $this->getAbsoluteUrl();
		return $meta;
	}

	/**
	* Determine if this Page has been published.
	*
	* @return bool
	*/
	/*public function isPublished($time=NULL) {
		$time = $time===NULL ? time() : $time;
		return !$this->hasExpired() && $this->datepublish>0 && $this->datepublish<=$time;
	}*/

	/**
	* Store the inner-template content for this Page.
	*
	* @param mixed An object
	* @return void
	*/
	/*public function setInnerContent($content) {

		// First, check if any changes to the content have actually taken place
		// otherwise create a new revision to store the content
		$encoded = json_encode($content);
		$cm = $this->getInnerContent();
		if($cm->isInDatabase() && json_encode($cm->content)==$encoded) {
			return $cm;
		}
		else if($cm->isInDatabase()) {
			$tpl = $cm->template;
			unset($cm);
			$cm = Model::create('PageContent');
			$cm->addRelatives($this);
			$cm->template = $tpl;
			$cm->type = ScribbleTemplate::TYPE_INNER;
			$cm->datecreated = time();
		}
		$cm->content = json_encode($content);
		return $cm;
	}*/

	/**
	* Store the outer-template content for this Page.
	*
	* @param mixed An object
	* @return void
	*/
	/*public function setOuterContent($content) {

		// First, check if any changes to the content have actually taken place
		// otherwise create a new revision to store the content
		$encoded = json_encode($content);
		$cm = $this->getOuterContent();
		if($cm->isInDatabase() && json_encode($cm->content)==$encoded) {
			return $cm;
		}
		else if($cm->isInDatabase()) {
			$tpl = $cm->template;
			unset($cm);
			$cm = Model::create('PageContent');
			$cm->addRelatives($this);
			$cm->template = $tpl;
			$cm->type = ScribbleTemplate::TYPE_OUTER;
			$cm->datecreated = time();
		}
		$cm->content = json_encode($content);
		return $cm;
	}*/

	/**
	* Convenience method for adding/removing tags associated with this Page.
	*
	* Any previously associated tags that are not listed in $tags will be returned
	* in a ModelCollection object so they can be handled as required (usually
	* deleted, or you may flag them in some way, etc).
	*
	* @param array List of tags to associate with this Page
	* @return ModelCollection
	*/
	/*public function setTagsArray($tags) {

		// Sanitize tags first so they're consistent across the system
		foreach($tags as $k=>$t) {
			$t = preg_replace("/[^a-z0-9\-\s]/", "", strtolower($t));
			$t = preg_replace("/^\s+/", "", preg_replace("/\s+$/", "", $t));
			$tags[$k] = $t;
		}

		// Load existing tags and remove any that are not in $tags
		$current = $this->findRelatives('TagPage');
		$disowned = array();
		foreach($current as $c) {
			if(!in_array($c->tag, $tags)) {
				$disowned[] = $c;
				$this->disownRelatives($c);
			}
			else {
				$this->addRelatives($c);
				$tags = array_diff($tags, array($c->tag));
			}
		}

		// Add all new tags
		foreach($tags as $t) {
			$m = Model::create('Tag');
			$m->tag = $t;
			$m->getModelManager()->load($m);
			$this->addRelatives($m);
		}

		// Return the list of disowned tags
		return new ModelCollection($disowned);
	}*/

	/**
	* Intercept calls to "$this->targetmenus = ...' to JSON-encode arrays.
	*
	* @param array|string Value
	* @return void
	*/
	/*public function setTargetmenus($val) {
		$this->dbData['targetmenus'] = is_array($val) ? json_encode($val) : $val;
	}*/

	/**
	* Intercepts calls to "$this->title = ..." in order to create a matching url
	* value.
	*
	* @param string The title
	* @return void
	*/
	public function setTitle($val) {
		$this->dbData['title'] = $val;
		$this->url = PageManager::convertToUrl($val);
	}
}
?>